﻿using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.IO.Ports;
using System.Linq;
using System.Management;
using System.Text.RegularExpressions;
using System.Threading;
using System.Windows;
using System.Windows.Threading;

namespace MDrive.ViewModels
{
    public class ComViewModel : ViewModelBase
    {
        private String _port;
        public String Port
        {
            get { return _port; }
            set
            {
                SetProperty(ref _port, value);
                if (_respondToPortChanges)
                {
                    Disconnect();
                    Connect();
                }
            }
        }

        private String _received;
        public String Received
        {
            get { return _received; }
            set { SetProperty(ref _received, value); }
        }

        private Boolean _useCheckSum;
        public Boolean UseCheckSum
        {
            get { return _useCheckSum; }
            set { SetProperty(ref _useCheckSum, value); }
        }

        private Boolean _busy;
        public Boolean Busy
        {
            get { return _busy; }
            set { SetProperty(ref _busy, value); }
        }

        private Boolean _sendingProgram;
        public Boolean SendingProgram
        {
            get { return _sendingProgram; }
            set { SetProperty(ref _sendingProgram, value); }
        }

        private Boolean _readingProgram;
        public Boolean ReadingProgram
        {
            get { return _readingProgram; }
            set { SetProperty(ref _readingProgram, value); }
        }

        private Boolean _sendRequest;
        public Boolean SendRequest
        {
            get { return _sendRequest; }
            set
            {
                SetProperty(ref _sendRequest, value);

                if (_sendRequest)
                {
                    EnQueue(ToSend);
                    _sendRequest = false;
                }
            }
        }

        private String _toSend;
        public String ToSend
        {
            get { return _toSend; }
            set { SetProperty(ref _toSend, value); }
        }

        private Boolean _autoPoll;
        public Boolean AutoPoll
        {
            get { return _autoPoll; }
            set { SetProperty(ref _autoPoll, value); }
        }

        public ObservableCollection<String> Ports { get; private set; }

        public ObservableCollection<HistoryLine> History { get; private set; }

        public ComViewModel(MDriveViewModel mDriveViewModel)
        {
            Ports = new ObservableCollection<String>();
            History = new ObservableCollection<HistoryLine>();

            _mDriveViewModel = mDriveViewModel;

            foreach (var mDriveParameterViewModel in _mDriveViewModel.MDriveParameterViewModels)
            {
                mDriveParameterViewModel.PropertyChanged += MDriveParameterPropertyChanged;
            }

            _mDriveViewModel.PropertyChanged += MDriveViewModelPropertyChanged;

            FindPorts();

            _respondToPortChanges = true;

            Connect();

            _dispatcherTimer.Tick += DispatcherTimerTick;
            _dispatcherTimer.Interval = TimeSpan.FromMilliseconds(100);
            _dispatcherTimer.Start();

            // jog
            // microstepping

            // manual errors
            // Syntax SM= (missing =)
            // A label is limited to to characters. (two tos)
            // 16 General purpose (should be 16 General purpose output)
            // Moving, output will be active while the moptor is in motion. (motor misspelled)
            // PR AL does not dump O1-O4
            // S1=20,0,0 ‘set IO point 2 to be a Velocity Changing ‘output, active when LOW (should be S2=...)
            // ben LED. T motor is a tonfi gured
        }

        public void EnQueue(String command)
        {
            if (command != null)
            {
                _queue.Enqueue(command);
                TrySend();
            }
        }

        void DispatcherTimerTick(object sender, EventArgs e)
        {
            if (_lastReceived + TimeSpan.FromSeconds(2) < DateTime.Now)
            {
                if (_buffer.Length > 0)
                {
                    Log(_buffer.Trim());
                    _buffer = String.Empty;
                }
            }

            if (_lastReceived + TimeSpan.FromSeconds(1) < DateTime.Now)
            {
                if (_buffer.Length == 0 && AutoPoll && _queue.Count == 0)
                {
                    EnQueue("PR AL");
                }
            }
        }

        void MDriveViewModelPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
        {
            if (_mDriveViewModel.ReadRequest)
            {
                AutoPoll = false;
                _mDriveViewModel.Program = String.Empty;
                _mDriveViewModel.ReadRequest = false;
                EnQueue("L");
            }

            if (_mDriveViewModel.SendRequest)
            {
                AutoPoll = false;
                EnQueue("CP");
                EnQueue("PG 1");
                foreach (var line in _mDriveViewModel.Program.Split(new[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries))
                {
                    EnQueue(line);
                }
                EnQueue("PG");
                _mDriveViewModel.SendRequest = false;
            }

            if (_mDriveViewModel.RunRequest)
            {
                AutoPoll = false;
                _mDriveViewModel.RunRequest = false;
                EnQueue("EX 1");
            }

            if (_mDriveViewModel.EndRequest)
            {
                AutoPoll = false;
                _mDriveViewModel.EndRequest = false;
                EnQueue("E");
            }
        }

        void MDriveParameterPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
        {
            var mDriveParameterViewModel = sender as MDriveParameterViewModel;
            if (mDriveParameterViewModel != null)
            {
                if (e.PropertyName.Equals("SendValue") || mDriveParameterViewModel.SendRequest)
                {
                    var separator = "=";
                    if (mDriveParameterViewModel.Command)
                    {
                        EnQueue("ER=0");
                        EnQueue("ST=0");
                        separator = " ";
                    }
                    EnQueue(mDriveParameterViewModel.Symbol + separator + mDriveParameterViewModel.SendValue);
                    mDriveParameterViewModel.SendRequest = false;
                }
            }
        }

        void TrySend()
        {
            if (!Busy && _queue.Count > 0 && _serialPort != null)
            {
                var command = _queue.Dequeue();

                Busy = true;

                Debug.WriteLine("Snd: " + command);

                if (command.Equals("PG 1"))
                {
                    SendingProgram = true;
                }

                if (command.Equals("L"))
                {
                    ReadingProgram = true;
                }

                if (UseCheckSum)
                {
                    var checkSumed = CheckSumed(command);
                    _serialPort.Write(checkSumed.ToArray(), 0, checkSumed.Count);
                }
                else
                {
                    _serialPort.Write(command + "\r");
                }
            }
        }

        List<Byte> CheckSumed(String command)
        {
            var retVal = command.Select(Convert.ToByte).ToList();
            var checkSum = (byte)command.Aggregate(0, (current, letter) => current + letter);
            checkSum = (byte)(~checkSum);
            checkSum++;
            checkSum |= 128;
            retVal.Add(checkSum);
            retVal.Add(Convert.ToByte('\r'));
            return retVal;
        }

        void SerialPortDataReceived(object sender, SerialDataReceivedEventArgs e)
        {
            var current = Application.Current;
            if (current != null && _serialPort != null)
            {
                current.Dispatcher.BeginInvoke(new Action(() => SerialPortDataReceived(_serialPort.ReadExisting().ToUpper())));
            }
        }

        void SerialPortDataReceived(String received)
        {
            _lastReceived = DateTime.Now;

            Debug.WriteLine("rcd: " + received);

            _buffer += received;

            if (SendingProgram)
            {
                SendingProgrammingDataReceived();
            }
            else if (ReadingProgram)
            {
                ReadingProgrammingDataReceived();
            }
            else
            {
                NormalDataReceived();
            }

            if (!Busy)
            {
                TrySend();
            }
        }

        void SendingProgrammingDataReceived()
        {
            // the prompt for the next line is either a line number or a ? followed by a line number
            var match = Regex.Match(_buffer, @"^[^\r\n]*\r\n\?? +[0-9]+ +", RegexOptions.None);

            // no end of transmission found
            if (!match.Success)
            {
                // check for end of programming
                match = Regex.Match(_buffer, @" *PG\r\n[>?]", RegexOptions.None);
                if (match.Success)
                {
                    SendingProgram = false;
                }
                else
                {
                    return;
                }
            }

            _buffer = _buffer.Substring(match.Length);

            Busy = false;
        }

        void ReadingProgrammingDataReceived()
        {
            // look for a full program listing - like "L\r\n\r\n  1  PR \"AAA\"\r\n  8  E \r\n\r\n?"
            var match = Regex.Match(_buffer, @"L(.*\r\n)+[>?]", RegexOptions.None);
            if (!match.Success)
            {
                return;
            }

            // split out the text before the end of transmission with anything that might have (unexpectedly) followed it.
            var response = _buffer.Substring(0, match.Length - 1);
            _buffer = _buffer.Substring(match.Length);

            _mDriveViewModel.Program = String.Empty;
            foreach (var instruction in response.Split(new[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries))
            {
                if (instruction.Length > 0 && !instruction.Equals("L"))
                {
                    // remove the line number and add the instruction to the program string
                    _mDriveViewModel.Program += Regex.Replace(instruction, @"  [0-9]+  ", "") + Environment.NewLine;
                }
            }

            ReadingProgram = false;
            Busy = false;
        }

        void NormalDataReceived()
        {
            var match = Regex.Match(_buffer, @"(.*\r\n)+[>?]", RegexOptions.None);

            // no end of transmission found
            if (!match.Success)
            {
                match = Regex.Match(_buffer, @".*\$", RegexOptions.Singleline);
                if (!match.Success)
                {
                    return;
                }
            }

            // the MDrive is no longer busy processing a command
            Busy = false;

            // split out the text before the end of transmission with anything that might have (unexpectedly) followed it.
            var response = _buffer.Substring(0, match.Length - 1);
            _buffer = _buffer.Substring(match.Length);

            // response to PR AL (print all variables)
            if (response.StartsWith("PR AL"))
            {
                response = response.Substring(5);

                // loop through each parameter
                foreach (var parameter in response.Split(new[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries))
                {
                    match = Regex.Match(parameter, @"([A-Za-z0-9]+) = ([GL] )?(-?[0-9]+)", RegexOptions.None); // G is for variables declaired with VA
                    if (match.Success)
                    {
                        SetParameter(match.Groups[1].Value, match.Groups[3].Value);
                    }
                }

                return;
            }

            // pick out assignments
            match = Regex.Match(response, @"([A-Za-z0-9]+)=(-?[0-9]+)", RegexOptions.None);
            if (match.Success)
            {
                SetParameter(match.Groups[1].Value, match.Groups[2].Value);
                return;
            }

            // all other responses
            Log(response.Trim());
        }

        private void SetParameter(string symbol, string value)
        {
            var mDriveParameterViewModel = _mDriveViewModel.FindMDriveParameterViewModel(symbol);
            if (mDriveParameterViewModel != null)
            {
                mDriveParameterViewModel.ReadValue = Parse(value);
            }
        }

        static int Parse(string value)
        {
            int retVal;
            if (int.TryParse(value, out retVal))
            {
                return retVal;
            }
            return 0;
        }

        void Connect()
        {
            if (!string.IsNullOrEmpty(Port))
            {
                try
                {
                    Busy = false;
                    AutoPoll = false;
                    ReadingProgram = false;
                    SendingProgram = false;
                    SendRequest = false;
                    _buffer = String.Empty;
                    _queue.Clear();

                    _mDriveViewModel.Program = String.Empty;

                    History.Clear();

                    _serialPort = new SerialPort(Port, 9600, Parity.None, 8, StopBits.One);
                    _serialPort.Open();
                    _serialPort.DataReceived += SerialPortDataReceived;

                    Log(Port + " open");

                    EnQueue("PG"); // exits program mode if needed
                    EnQueue("E"); // ends any running programs
                    EnQueue("EE=1"); // enables the encoder
                    EnQueue("SL 0"); // stops movement
                    EnQueue("ER=0"); // clears errors
                    EnQueue("ST=0"); // clears stall
                    EnQueue("PR AL"); // get all parameters
                    EnQueue("L"); // get program listing
                }
                catch (Exception e)
                {
                    Log(Port + " failed");
                }
            }
        }

        void Disconnect()
        {
            if (_serialPort != null)
            {
                _serialPort.DataReceived -= SerialPortDataReceived;
                _serialPort.Close();
                _serialPort = null;
            }
        }

        void ResetToFactoryDefaults()
        {
            foreach (var baud in new[] { 4800, 9600, 19200, 38400, 115200 })
            {
                _serialPort.BaudRate = baud;
                Thread.Sleep(300);
                _serialPort.Write("\rCK=0\x85\r\rFD\r\n*FD\n\n*FD\xCC\n\r");
                Thread.Sleep(300);
            }

            _serialPort.BaudRate = 9600;
        }

        void FindPorts()
        {
            var ports = new List<String>();

            // see http://msdn.microsoft.com/en-us/library/windows/desktop/aa394084(v=vs.85).aspx
            using (var searcher = new ManagementObjectSearcher(@"Select * From Win32_SerialPort"))
            {
                foreach (var device in searcher.Get())
                {
                    var name = device["Name"];
                    var deviceID = device["DeviceID"];
                    if (name != null &&
                        deviceID != null &&
                        name.Equals("IMS USB to RS422 Converter Cable"))
                    {
                        ports.Add(deviceID.ToString());
                    }
                }
            }

            using (var searcher = new ManagementObjectSearcher(@"Select * From Win32_PnPEntity"))
            {
                foreach (var device in searcher.Get())
                {
                    var name = device["Name"];
                    var manufacturer = device["Manufacturer"];
                    var description = device["Description"];
                    if (name != null &&
                        manufacturer != null &&
                        description != null &&
                        manufacturer.Equals("FTDI") &&
                        description.Equals("USB Serial Port"))
                    {
                        var match = Regex.Match(name.ToString(), @"USB Serial Port \((COM[0-9]+)\)");
                        if (match.Success)
                        {
                            ports.Add(match.Groups[1].Value);
                        }
                    }
                }
            }

            ports.Sort();

            foreach (var port in ports)
            {
                Ports.Add(port);
            }

            if (Ports.Count > 0)
            {
                Port = Ports[0];
            }
        }

        void Log(String s)
        {
            if (!string.IsNullOrEmpty(s))
            {
                History.Add(new HistoryLine(s));

                if (History.Count > 200)
                {
                    History.RemoveAt(0);
                }
            }
        }

        readonly MDriveViewModel _mDriveViewModel;
        readonly DispatcherTimer _dispatcherTimer = new DispatcherTimer();
        readonly Queue<String> _queue = new Queue<string>();
        DateTime _lastReceived;
        SerialPort _serialPort;
        string _buffer = String.Empty;
        private Boolean _respondToPortChanges = false;
    }

    public class HistoryLine
    {
        public HistoryLine(String s)
        {
            _string = s;
        }

        public override string ToString()
        {
            return _string;
        }

        private readonly String _string;
    }
}
